/// <reference lib="es2016" />

import json = require('json');
import util = require('util');

import {
    fill_error,
    err_info,
    err_info_msg,
    make_err_message,
    render_error,
    APPError
} from "./err_info";
import { ouputMap } from './mimes';
import { default_session_for_acl } from './checkout_acl';
import { FibApp } from '../Typo/app';
import { Helpers } from '@fxjs/orm';

const _slice = Array.prototype.slice;

function write_success(result: FibApp.FibAppResponse, req: FibApp.FibAppHttpRequest) {
    if (result.success)
        req.response.write(result.success);
}

function fill_undefined_error(req: FibApp.FibAppHttpRequest, cls: FibApp.FibAppORMModel): void {
    fill_error(req, err_info(5000003, {
        classname: cls.model_name,
    }, cls ? cls.cid : -1));
}

export const filterRequest: FibApp.FibAppClass['filterRequest'] = function (
    this: FibApp.FibAppClass,
    app_httprequest: FibApp.FibAppHttpRequest,
    classname: string
) {
    const arglen = arguments.length;
    const earg = _slice.call(arguments, 2, arglen - 1);
    const handler: FibApp.FibAppFunctionToBeFilter = arguments[arglen - 1];

    const app = this;

    return app.dbPool((db: FibApp.FibAppORM) => {
        let data;

        // check empty data
        if (app_httprequest.length == 0 && handler.length === arglen + 1)
            return fill_error(app_httprequest,
                err_info(4000001, {
                    method: app_httprequest.method
                }));

        // decode json data
        if (app_httprequest.length > 0)
            try {
                data = app_httprequest.json();
            } catch (e) {
                return fill_error(app_httprequest, err_info(4000002));
            }

        // check classname
        let cls: FibApp.FibAppORMModel = null
        if (classname) {
            cls = db.models[classname];
            if (cls === undefined)
                return fill_error(app_httprequest,
                    err_info(4040001, {
                        classname: classname
                    }));

            /**
             * dont support table has more than 1 key, such as 
             * - user customized
             * - generated by orm internal association, such as mergeTable in many-association.
             */
            if (cls.settings.get('rest.model.disable_access_composite_table') && cls.id.length > 1)
                return fill_error(app_httprequest,
                    err_info(4040001, {
                        classname: classname
                    }));
        }

        const _req = makeFibAppReqInfo(
            app_httprequest,
            app,
            {
                classname,
                handler,
                extend_args: earg
            })

        try {
            let where = normalizeQueryWhere(_req, cls);
            if (where) {
                _req.query.where = where
            }

            let findby = normalizeQueryFindBy(_req, cls);
            if (findby) {
                _req.query.findby = findby
            }
        } catch (error) {
            if (error instanceof APPError) {
                switch (error.code) {
                    case 'INVALID_QUERY_WHERE':
                        return fill_error(app_httprequest, err_info(4000003));
                    case 'INVALID_QUERY_WHERE_KEY':
                        return fill_error(app_httprequest, err_info_msg(4000006, error.message));
                    case 'INVALID_QUERY_FINDBY':
                        return fill_error(app_httprequest, err_info(4000007));
                    case 'INVALID_QUERY_FINDBY_EXTEND':
                        return fill_error(app_httprequest, err_info_msg(4000008, error.message));
                    case 'INVALID_FINDBY_WHERE_KEY':
                        return fill_error(app_httprequest, err_info_msg(4000009, error.message));
                }
            }
            
            throw error;
        }

        let result: FibApp.FibAppResponse = null;
        try {
            result = handler.apply(undefined, ([_req, db, cls] as any[]).concat(earg, [data]));
        } catch (e) {
            if (!this.__opts.hideErrorStack)
                console.error(e.stack);
            if (e.type === 'validation') {
                result = {
                    error: {
                        code: 5000000,
                        message: e.msg
                    }
                };
            } else {
                return fill_error(app_httprequest, err_info(5000002, {
                    function: "func",
                    classname: classname,
                    message: e.message
                }, cls ? cls.cid : -1));
            }
        }

        if (result.status)
            app_httprequest.response.statusCode = result.status;

        switch (_req.req_resource_type) {
            case 'json':
                if (result.error) {
                    fill_error(app_httprequest, result);
                } else if (result.hasOwnProperty('success')) {
                    app_httprequest.response.json(result.success);
                } else {
                    fill_undefined_error(app_httprequest, cls)
                }

                break
            case 'css':
                write_success(result, app_httprequest);
                break
            case 'js':
                write_success(result, app_httprequest);
                break
            case 'html':
                if (result.error) {
                    render_error(app_httprequest, result);
                } else if (result.hasOwnProperty('success')) {
                    switch (typeof result.success) {
                        default:
                            write_success(result, app_httprequest);
                            break
                        case 'object':
                            app_httprequest.response.json(result.success);
                            break
                    }
                } else {
                    fill_undefined_error(app_httprequest, cls)
                }

                break
        }

        _req.response_headers = util.extend({
            'Content-Type': ouputMap[_req.req_resource_type] || ouputMap.json
        }, _req.response_headers)
        
        if (_req && _req.response_headers)
            app_httprequest.response.setHeader(_req.response_headers);
    });
}

function checkFilterWhere (whereBlacklist: Set<string>, _where: FibApp.ReqWhere, options?: {
    extend: string
}) {
    Object.keys(_where).forEach(key => {
        switch (key) {
            case 'or':
                return (_where[key] as FibApp.ReqWhere[]).forEach?.(item => checkFilterWhere(whereBlacklist, item, options));
            default:
                if (whereBlacklist.has(key)) {
                    if (!options?.extend) {
                        throw (new APPError('INVALID_QUERY_WHERE_KEY', make_err_message('4000006', { key })));
                    } else {
                        throw (new APPError('INVALID_FINDBY_WHERE_KEY', make_err_message('4000009', { key, extend: options?.extend })));
                    }
                }
        }
    });
}

export function normalizeQueryWhere (
    _req: FibApp.FibAppReq,
    model: FibApp.FibAppORMModel
): FibApp.ReqWhere {
    let _where;
    if (_req.query.where !== undefined)
        try {
            _where = json.decode(_req.query.where as string);
        } catch (error) {
            throw (new APPError('INVALID_QUERY_WHERE', error.message));
        }
        
    if (_where) {
        checkFilterWhere(model.$webx.__whereBlackProperties, _where);
    }

    return _where
}

function checkFilterFindByItem (model: FibApp.FibAppORMModel, _findby: FibApp.ReqFindByItem) {
    const findByBlacklist = model.$webx.__findByExtendBlackProperties;

    if (findByBlacklist.has(_findby.extend)) {
        throw (new APPError('INVALID_QUERY_FINDBY_EXTEND', make_err_message('4000008', { extend: _findby.extend })));
    }

    const assoc = Helpers.tryGetAssociationItemFromModel(_findby.extend, model);
    if (assoc) {
        checkFilterWhere(assoc.model.$webx.__whereBlackProperties, _findby.where, { extend: _findby.extend });
    }
}

export function normalizeQueryFindBy (
    _req: FibApp.FibAppReq,
    model: FibApp.FibAppORMModel
): FibApp.ReqFindByItem {
    let _findby;
    if (_req.query.findby !== undefined)
        try {
            if (typeof _req.query.findby === 'string') {
                _findby = json.decode(_req.query.findby as any as string);
            }
        } catch (error) {
            throw (new APPError('INVALID_QUERY_FINDBY', error.message));
        }
        
    if (_findby) {
        checkFilterFindByItem(model, _findby);
    }

    return _findby
}

export function makeFibAppReqInfo (
    orequest: FibApp.FibAppHttpRequest,
    app: FibApp.FibAppClass,
    {
        classname = null,
        handler = null,
        extend_args: earg = []
    }: {
        classname: string,
        handler?: FibApp.FibAppFunctionToBeFilter,
        extend_args?: [
            /* extend_id | extend_instance */
            (FibApp.AppIdType | FibApp.FibDataPayload)?,
            /* extend_name */
            string?,
            /* rid */
            FibApp.AppIdType?,
        ]
    }
): FibApp.FibAppReq {
    const _req: FibApp.FibAppReq = {
        session: default_session_for_acl(orequest.session as FibApp.FibAppSession),
        query: (
            // TODO: fixup confusing action of HttpCollection in fibjs 0.30.0, 0.31.0
            typeof (orequest.query as any).keys === 'function' ? orequest.query.all(undefined) as any
            : orequest.query.toJSON()
        ),
        request: orequest,

        req_resource_type: parse_req_resource_and_hdlr_type(orequest).requestedResultType,
        req_resource_basecls: classname,
        req_resource_extend: undefined,
        req_resource_handler_type: undefined
    }

    if (handler) {
        if (is_internal_base_api_fn(app, handler)) {
            _req.req_resource_handler_type = 'builtInBaseRest'
        } else if (is_internal_ext_api_fn(app, handler)) {
            _req.req_resource_handler_type = 'builtInExtRest'
            _req.req_resource_extend = earg[1]
        } else {
            _req.req_resource_handler_type = 'modelFunction'
        }
    }

    const keys = _req.query.keys;
    if (keys !== undefined && typeof keys === 'string')
        _req.query.keys = keys.split(',');

    return _req
}

function is_internal_base_api_fn(app: FibApp.FibAppClass, func: any | Function) {
    return [app.api.get, app.api.post, app.api.put, app.api.del, app.api.find].includes(func)
}

function is_internal_ext_api_fn(app: FibApp.FibAppClass, func: any | Function) {
    return [app.api.eget, app.api.epost, app.api.eput, app.api.edel, app.api.efind].includes(func)
}

export function parse_req_resource_and_hdlr_type(req: Class_HttpRequest): {
    requestedResultType: FibApp.FibAppReqResourceType,
    requestedPayloadType: FibApp.FibAppReqResourceHandlerType
} {
    const reqAcceptString = (req.firstHeader('Accept') || '').split(';')[0] || ''
    const reqContentTypeString = (req.firstHeader('Content-Type') || '').split(';')[0] || ''

    const contentTypeString = reqAcceptString.split(',')[0] || reqContentTypeString || '';

    let requestedResultType: FibApp.FibAppReqResourceType = 'json'
    let requestedPayloadType: FibApp.FibAppReqResourceHandlerType = 'unknown'

    switch (contentTypeString) {
        case 'application/graphql':
            requestedResultType = 'json'
            requestedPayloadType = 'graphql'
            break
        case 'application/json':
            requestedResultType = 'json'
            break
        case 'application/javascript':
        case 'text/javascript':
            requestedResultType = 'js'
            break
        case 'text/html':
        case 'text/xhtml':
            requestedResultType = 'html'
            break
        case 'text/css':
            requestedResultType = 'css'
            break
        default:
            requestedResultType = 'json'
            requestedPayloadType = requestedPayloadType || 'unknown'
            break
    }

    return {
        requestedResultType,
        requestedPayloadType
    }
}